package lexer;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * Class that implements the lexer of the SO programming language
 * 
 * @author sebastian
 * 
 */
public class Lexer implements Iterator<Token> {

	/**
	 * Source code as String
	 */
	private String programCode;
	
	/**
	 * Source code as character array
	 */
	private char[] characterArray;
	
	/**
	 * List of the tokens which represents the source code
	 */
	private List<Token> tokenList = new ArrayList<Token>();
	
	/**
	 * Iterator, provides an interface to use this lexer in a compiler (or parser)
	 */
	private Iterator<Token> myIterator;
	
	/**
	 * Internal pointer to the current character
	 */
	private int charArrayPointer = 0;
	
	/**
	 * Line position in the source code 
	 * of the token which represents the current character
	 */
	private int col = 1;
	
	/**
	 * Column position in the source code 
	 * of the token which represents the current character
	 */
	private int line = 1;
	
	/**
	 * Array that provides all possible Token Types
	 */
	private TokenType[] allTypes = TokenType.values();

	/**
	 * Constructor, lexes the given source code and provides it for other classes by
	 * implementing an iterator and filling it
	 * @param programCode the source code
	 */
	public Lexer(String programCode) {
		this.programCode = programCode;
		this.characterArray = this.programCode.toCharArray();
		this.charArrayPointer = 0;
		read();
		this.myIterator = this.tokenList.iterator();
	}

	/**
	 * Method who skips all white spaces like \n, \t, " " 
	 * at the current position in the source code
	 */
	private void skipWhiteSpace() {
		if (this.characterArray[this.charArrayPointer] == '\n') {
			this.charArrayPointer++;
			this.col = 1;
			this.line++;
		} else if (this.characterArray[this.charArrayPointer] == '\t') {
			this.charArrayPointer++;
			this.col += 4;
		} else if (this.characterArray[this.charArrayPointer] == ' ') {
			this.charArrayPointer++;
			this.col++;
		}
	}

	/**
	 * Method who skips all comments 
	 * at the current position in the source code, e.g.:
	 * # one line comment
	 * /+ stage 1 /+ stage 2 /+ nested comments +/ one more comment +/ +/
	 */
	private void skipComment() {
		int ebene = 0;
		if (this.characterArray[this.charArrayPointer] == '#') {
			while (this.characterArray[this.charArrayPointer] != '\n') {
				this.charArrayPointer++;
				if (this.charArrayPointer >= this.characterArray.length)
					break;
			}
			this.charArrayPointer++;
			this.line++;
			this.col = 1;
		}
		if ((this.charArrayPointer + 1 < this.characterArray.length)
				&& (this.characterArray[this.charArrayPointer] == '/')
				&& (this.characterArray[this.charArrayPointer + 1] == '+')) {
			ebene++;
			this.charArrayPointer += 2;
			while ((this.charArrayPointer + 1 < this.characterArray.length)
					&& (ebene > 0)) {
				if ((this.charArrayPointer + 1 < this.characterArray.length)
						&& (this.characterArray[this.charArrayPointer] == '/')
						&& (this.characterArray[this.charArrayPointer + 1] == '+')) {
					ebene++;
					this.charArrayPointer++;
				}
				if ((this.charArrayPointer + 1 < this.characterArray.length)
						&& (this.characterArray[this.charArrayPointer] == '+')
						&& (this.characterArray[this.charArrayPointer + 1] == '/')) {
					ebene--;
					this.charArrayPointer++;
				}
				this.charArrayPointer++;
			}
			if (ebene == 0)
				return;
			this.charArrayPointer++;
		}
	}

	/**
	 * Skips all white spaces or comment beginning at the current position
	 */
	private void skipWhiteSpaceAndComments() {
		try {
			while (Character
					.isWhitespace(this.characterArray[this.charArrayPointer])
					|| this.characterArray[this.charArrayPointer] == '#'
					|| (this.characterArray[this.charArrayPointer] == '/' &&
							this.characterArray[this.charArrayPointer+1] == '+')) {
				skipWhiteSpace();
				if (this.charArrayPointer >= this.characterArray.length)
					break;
				skipComment();
				if (this.charArrayPointer >= this.characterArray.length)
					break;
			}
		}
		catch(ArrayIndexOutOfBoundsException e) {
			return;
		}
	}

	/**
	 * Reads a single Token that begins at the current position.
	 * This may be literals like integer, long, float, double, char, String literals.
	 * But also key words like return or any identifier
	 * Before this method returns, it increases line, col, charArrayPointer 
	 * 
	 * @return the token that starts at the current position
	 */
	private Token readSingleToken() {
		// Parsing Char Literal
		if (this.characterArray[this.charArrayPointer] == '\'') {
			StringBuilder builder = new StringBuilder();
			int col = this.col;
			int line = this.line;
			this.charArrayPointer++;
			this.col++;
			if (this.charArrayPointer < this.characterArray.length) {
				builder.append(this.characterArray[this.charArrayPointer]);
				this.charArrayPointer++;
				this.col++;
				if (this.charArrayPointer < this.characterArray.length) {
					if (builder.toString().intern().equals("\\")) {
						builder.append(this.characterArray[this.charArrayPointer]);
						this.charArrayPointer++;
						this.col++;
					}
					if (this.charArrayPointer < this.characterArray.length) {
						assert (this.characterArray[this.charArrayPointer] == '\'') :
							"Line: " + line + " Character can only have one letter!";
						this.charArrayPointer++;
						this.col++;
					}
				}
			}
			return new Token(line, col, TokenType.CHAR_LITERAL, builder.toString().intern());
		}
		// END parsing Char Literal

		// Parsing String Literal
		if (this.characterArray[this.charArrayPointer] == '\"') {
			StringBuilder builder = new StringBuilder();
			int col = this.col;
			int line = this.line;
			this.charArrayPointer++;
			this.col++;
			if (this.charArrayPointer < this.characterArray.length) {
				while (this.characterArray[this.charArrayPointer] != '\"') {
					builder.append(this.characterArray[this.charArrayPointer]);
					this.charArrayPointer++;
					this.col++;
					if (this.charArrayPointer < this.characterArray.length) {
						if (builder.charAt(builder.length()-1) == '\\') {
							builder.append(this.characterArray[this.charArrayPointer]);
							this.charArrayPointer++;
							this.col++;
						}
					}
					if (this.charArrayPointer >= this.characterArray.length)
						break;
				}
				this.charArrayPointer++;
			}
			return new Token(line, col, TokenType.STRING_LITERAL, builder.toString().intern());
		}
		// END parsing String Literal

		// Parsing Identifier, an identifier is something like that
		// identifier := ( _ | number | letter ) [identifier]
		else if ((Character.isLetter(this.characterArray[this.charArrayPointer]))
				|| (this.characterArray[this.charArrayPointer] == '_')) {
			StringBuilder builder = new StringBuilder();
			int col = this.col;
			int line = this.line;
			while (Character
					.isLetterOrDigit(this.characterArray[this.charArrayPointer])
					|| (this.characterArray[this.charArrayPointer] == '_')) {
				builder.append(this.characterArray[this.charArrayPointer]);
				this.charArrayPointer++;
				this.col++;
				if (this.charArrayPointer >= this.characterArray.length)
					break;
			}
			String text = builder.toString().intern();
			for (TokenType tt : this.allTypes) {
				if (tt.isKeyword() && text.equals(tt.getText())) {
					return new Token(line, col, tt, text);
				}
			}
			return new Token(line, col, TokenType.IDENT, text);
		}
		// END parsing identifiers

		// Start parsing numberLiterals like float, double, int, long
		else if (Character.isDigit(this.characterArray[this.charArrayPointer])) {
			StringBuilder builder = new StringBuilder();
			int col = this.col;
			int line = this.line;
			while (Character
					.isDigit(this.characterArray[this.charArrayPointer])) {
				builder.append(this.characterArray[this.charArrayPointer]);
				this.charArrayPointer++;
				this.col++;
				if (this.charArrayPointer >= this.characterArray.length)
					break;
			}
			if (this.charArrayPointer < this.characterArray.length) {
				if (this.characterArray[this.charArrayPointer] == '.') {
					builder.append('.');
					this.charArrayPointer++;
					this.col++;
					if (this.charArrayPointer < this.characterArray.length) {
						while (Character
								.isDigit(this.characterArray[this.charArrayPointer])) {
							builder
									.append(this.characterArray[this.charArrayPointer]);
							this.charArrayPointer++;
							this.col++;
							if (this.charArrayPointer >= this.characterArray.length)
								break;
						}
					} else {
						builder.append('0');
					}
					if (this.charArrayPointer < this.characterArray.length) {
						if ((this.characterArray[this.charArrayPointer] == 'F')
								|| (this.characterArray[this.charArrayPointer] == 'f')) {
							builder
									.append(this.characterArray[this.charArrayPointer]);
							this.charArrayPointer++;
							String text = builder.toString().intern();
							return new Token(line, col,
									TokenType.FLOAT_LITERAL, text);
						}
					}
					String text = builder.toString().intern();
					return new Token(line, col, TokenType.DOUBLE_LITERAL, text);
				}
			}
			if (this.charArrayPointer < this.characterArray.length) {
				if ((this.characterArray[this.charArrayPointer] == 'L')
						|| (this.characterArray[this.charArrayPointer] == 'L')) {
					builder.append(this.characterArray[this.charArrayPointer]);
					this.charArrayPointer++;
					String text = builder.toString().intern();
					return new Token(line, col, TokenType.LONG_LITERAL, text);
				}
			}
			String text = builder.toString().intern();
			return new Token(line, col, TokenType.INT_LITERAL, text);
		}
		// END parsing numbers

		// Parsing other Tokens, like seperators
		else {
			int line = this.line;
			int col = this.col;
			for (int i = getMaxTokenType(); i > 0; i--) {
				try {
					StringBuilder builder = new StringBuilder();
					for (int j = 0; j < i; j++) {
						builder
								.append(this.characterArray[this.charArrayPointer
										+ j]);
					}
					String test = builder.toString().intern();
					for (TokenType tt : this.allTypes) {
						if (tt.getText().equals(test)) {
							this.charArrayPointer += i;
							this.col += i;
							return new Token(line, col, tt, "");
						}
					}
				} catch (ArrayIndexOutOfBoundsException e) {
				}
			}
			this.charArrayPointer++;
			this.col++;
			return new Token(line, col, TokenType.ERR, "");
		}
		// END parsing other tokens
	}

	/**
	 * Returns the maximal possible length of a keyword Token
	 * @return the maximal length
	 */
	private int getMaxTokenType() {
		int result = 0;
		for (TokenType tt : this.allTypes) {
			if ((tt.getText().length() > result) && tt.isKeyword()) {
				result = tt.getText().length();
			}
		}
		return result;
	}

	/**
	 * Transforms the source code to a List of Tokens
	 * Catches all Exception and converts them to an internal compiler problem
	 */
	private void read() {
		try {
			while (this.charArrayPointer < this.characterArray.length) {
				this.skipWhiteSpaceAndComments();
				if (this.charArrayPointer < this.characterArray.length) {
					this.tokenList.add(readSingleToken());
				}
			}
			this.tokenList.add(new Token(this.line, this.col, TokenType.EOF, ""));
		}
		catch (Exception e) {
			System.err.println("The lexer causes an internal problem. Please ask " +
					"the project members or ask at saarinformatix [at] googlemail [dot] com");
			System.exit(-1);
		}
	}

	/**
	 * Returns the tokenList and so it provides an alternative interface to provide the tokens
	 * (without using an iterator=
	 * @return the lexed Token-List
	 */
	public List<Token> getTokenList() {
		return this.tokenList;
	}
	
	/* (non-Javadoc)
	 * @see java.util.Iterator#hasNext()
	 */
	public boolean hasNext() {
		return this.myIterator.hasNext();
	}

	/* (non-Javadoc)
	 * @see java.util.Iterator#next()
	 */
	public Token next() {
		return this.myIterator.next();
	}

	/* (non-Javadoc)
	 * @see java.util.Iterator#remove()
	 */
	public void remove() {
		throw new UnsupportedOperationException(
				"Lexer doesn't support the remove option.");
	}

}
